package edu.neu.cs6515.admin;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import javax.swing.Timer;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.java.contract.ContractAssertionError;
import com.google.java.contract.Ensures;
import com.google.java.contract.Requires;

import edu.neu.cs6515.board.HexagonBoard;
import edu.neu.cs6515.board.ScoreCalc;
import edu.neu.cs6515.player.IPlayer;
import edu.neu.cs6515.player.strategy.StrategyException;
import edu.neu.cs6515.turn.ITurnProxy;
import edu.neu.cs6515.turn.TurnProxy;
import edu.neu.cs6515.xml.IngeniousColor;
import edu.neu.cs6515.xml.Placement;
import edu.neu.cs6515.xml.Score;
import edu.neu.cs6515.xml.Tile;

/**
 * Administrator for Ingenious Games: Allows players to register and then runs a
 * game
 */
public class Administrator {

    private static final int MIN_REQUIRED_PLAYERS =
            2;
    private static final int MAX_PLAYERS = 6;
    public static final int INITIAL_TILES_IN_HAND =
            6;
    public static final int INITIAL_TILES_IN_BAG =
            120;

    private List<PlayerEntry> playerTable =
            new LinkedList<PlayerEntry>();
    private HexagonBoard board;
    private TileBag tileBag;
    private final int nbrOfPlayers;
    public static final int NBR_OF_COLORS = 6;

    /**
     * Initializes Admin with random tile bag and nbrOfPlayers
     * 
     * @param nbrOfPlayers
     *            number of players for the game
     */
    @Requires({ "nbrOfPlayers <= getMaxPlayers()",
            "nbrOfPlayers >= getMinPlayers()" })
    public Administrator(final int nbrOfPlayers) {
        this(new TileBag(INITIAL_TILES_IN_BAG),
                nbrOfPlayers);
    }

    @Requires({ "tileBag != null",
            "nbrOfPlayers <= getMaxPlayers()",
            "nbrOfPlayers >= getMinPlayers()" })
    public Administrator(final TileBag tileBag, final int nbrOfPlayers) {
        this.tileBag = tileBag;
        this.nbrOfPlayers = nbrOfPlayers;
    }

    /**
     * Initializes Admin with given list of tiles (bag)
     * 
     * @param tileBag
     *            list of tiles for admin
     * @param nbrOfPlayers
     *            number of players for the game
     */
    @Requires({ "tiles != null",
            "nbrOfPlayers <= getMaxPlayers()",
            "nbrOfPlayers >= getMinPlayers()" })
    public Administrator(
            final List<Tile> tiles,
            final int nbrOfPlayers) {
        this(new TileBag(tiles), nbrOfPlayers);
    }

    /**
     * Registers a new game participant.
     * 
     * NOTE: Name is only in sig because of the P8 requirement, a player already
     * has a name!
     * 
     * @param player
     *            Player to be registered in the game
     * @param playerName
     *            Name of the player to be registered
     */
    @Requires({ "getPlayerTableSize() < getNbrOfPlayers()",
            "!isPlayerNameAlreadyRegistered(playerName)" })
    @Ensures({ "getPlayerTableSize() == old(getPlayerTableSize()) + 1",
            "getPlayerTableSize() <= getNbrOfPlayers()" })
    public void registerPlayer(
            final IPlayer player,
            final String playerName) {
        PlayerEntry newPlayerEntry =
                new PlayerEntry(player,
                        playerName,
                        new Score(),
                        prepareRandomTilesForPlayers());
        playerTable.add(newPlayerEntry);
        player.registerAccepted(
                nbrOfPlayers,
                newPlayerEntry.getTilesInHand());
    }

    /**
     * COFOJ unfortunately ignores constants in contracts
     * 
     * @return MAX_PLAYERS
     */
    public static int getMaxPlayers() {
        return MAX_PLAYERS;
    }

    /**
     * same as getMaxPlayers()
     * 
     * @return MIN_REQUIRED_PLAYERS
     */
    public static int getMinPlayers() {
        return MIN_REQUIRED_PLAYERS;
    }

    /**
     * same as getMaxPlayers()
     * 
     * @return INITIAL_TILES_IN_HAND
     */
    public static int getInitialNumberOfTilesInHand() {
        return INITIAL_TILES_IN_HAND;
    }

    public int getNumberOfPlacementsInGame() {
        return board.getAllPlacementsOnBoard()
                .size();
    }

    /**
     * @return List of random tiles that are taken out of the bag and can be
     *         handed to a player
     */
    private List<Tile> prepareRandomTilesForPlayers() {
        List<Tile> randomTilesForPlayer =
                new LinkedList<Tile>();
        for (int i = 0; i < INITIAL_TILES_IN_HAND; i++) {
            if (tileBag.getNumberOfTiles() > 0) {
                randomTilesForPlayer.add(tileBag.drawATile());
            }
        }
        return randomTilesForPlayer;
    }

    /**
     * @param playerName
     *            player's name to be checked
     * @return true if the player name is already registered in the admin
     */
    @Requires("playerName != null")
    public boolean isPlayerNameAlreadyRegistered(
            final String playerName) {
        for (PlayerEntry entry : playerTable) {
            if (entry.getName()
                    .equals(playerName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if the number of registered players reached the preassigned
     *         number of players
     */
    public boolean enoughPlayerRegistred() {
        return playerTable.size() == this.getNbrOfPlayers();
    }

    /**
     * Runs a single game with all registered participants
     * 
     * @return List of ranking entries that informs about the outcome of a
     *         single game
     */
    @Requires("enoughPlayerRegistred()")
    @Ensures("result != null")
    public List<RankingEntry> runGame() {
        prepareBoardAndInformPlayers();
        while (true) {
            for (PlayerEntry gamePlayer : playerTable) {
                if (gameIsOver()) {
                    return generateRankings();
                } else if (gamePlayer.getTilesInHand()
                        .size() > 0
                        && gamePlayer.isPlayerAllowedToPlay()) {
                    executeOneTurn(gamePlayer);
                }
            }
        }
    }

    /**
     * Creates game board and informs each player about the name of other
     * participants
     */
    private void prepareBoardAndInformPlayers() {
        board = new HexagonBoard(nbrOfPlayers);
        for (PlayerEntry playerEntry : playerTable) {
            List<PlayerEntry> entriesToSend = new LinkedList<PlayerEntry>(playerTable);
            entriesToSend.remove(playerEntry);
            List<String> namesToSend = new LinkedList<String>();
            for (PlayerEntry entryToSend : entriesToSend) {
                namesToSend.add(entryToSend.getName());
            }
            playerEntry.getPlayer().setPlayersInGame(namesToSend);
        }
    }

    /**
     * Asks the gamePlayer to take one turn
     * 
     * @param gamePlayer
     *            player whose turn it currently is
     */
    private void executeOneTurn(
            final PlayerEntry gamePlayer) {
        HexagonBoard boardForThisTurn =
                new HexagonBoard(this.board.getNbrOfPlayers());
        boardForThisTurn.updateHexagonBoard(this.board.getAllPlacementsOnBoard());
        ITurnProxy proxy =
                new TurnProxy(boardForThisTurn,
                        gamePlayer.getTilesInHand(),
                        gamePlayer.getScore(),
                        tileBag);
        List<Placement> placementToBroadCast = new LinkedList<Placement>();
        try {
            gamePlayer.getPlayer().makeYourTurn(
                    proxy);
            commitTurnProxy(gamePlayer, proxy);
            placementToBroadCast.addAll(proxy.getPlacementsInThisTurn());
        } catch (Throwable e) {
			e.printStackTrace();
            gamePlayer.disqualifyPlayer(e.getMessage());
        } finally {
            broadcastLastTurn(gamePlayer, placementToBroadCast);
        }
    }

    /**
     * commit the proxy to change the state of the real board
     * 
     * @param gamePlayer
     *            player taking last turn
     * @param proxy
     *            proxy of last turn
     */
    private void commitTurnProxy(
            final PlayerEntry gamePlayer,
            final ITurnProxy proxy) {
        for (Placement placement : proxy.getPlacementsInThisTurn()) {
            this.board.updateHexagonBoard(placement);
        }
        gamePlayer.setScore(proxy.getCurrentScore());
        gamePlayer.setTilesInHand(proxy.getAllTilesInPlayersHand());
    }

    /**
     * broadcast last turn to all qualified players in game
     * 
     * @param gamePlayer
     *            proxy of last turn
     * @param placementToBroadCast
     *            player taking last turn
     */
    private void broadcastLastTurn(
            final PlayerEntry gamePlayer,
            final List<Placement> placementToBroadCast) {
        for (PlayerEntry entry : playerTable) {
            if (entry.isPlayerAllowedToPlay()) {
                entry.getPlayer()
                        .recieveTurnBroadcast(
                                placementToBroadCast,
                                gamePlayer.getName());
            }
        }
    }

    /**
     * @return sorted list of RankingEntry containing survival players in game
     */
    private List<RankingEntry> generateRankings() {
        Collections.sort(playerTable);
        Collections.reverse(playerTable);
        List<RankingEntry> rankingEntries =
                new ArrayList<RankingEntry>();
        int rank = 0;
        PlayerEntry previous = null;
        for (PlayerEntry playerEntry : playerTable) {
            if (playerEntry.isPlayerAllowedToPlay()) {
                rank =
                        increaseRankIfBetterThanPrevious(
                                rank,
                                previous,
                                playerEntry);
                rankingEntries.add(new RankingEntry(playerEntry.getPlayer(),
                        playerEntry.getName(),
                        playerEntry.getScore(),
                        rank));
                previous = playerEntry;
            }
        }
        return rankingEntries;
    }

    /**
     * @param rank
     *            current rank
     * @param previous
     *            previous player that was ranked
     * @param playerEntry
     *            current player to be ranked
     * @return increased rank if there was no previous entry of if this entry
     *         was better
     */
    private int increaseRankIfBetterThanPrevious(
            final int rank,
            final PlayerEntry previous,
            final PlayerEntry playerEntry) {
        if (previous == null
                || previous.getScore().compareTo(
                        playerEntry.getScore()) != 0) {
            return rank + 1;
        }
        return rank;
    }

    /**
     * @return true if the game is over and the final ranking can be caluclated
     */
    private boolean gameIsOver() {
        if (getNumberOfQualifiedPlayers() == 0) {
            return true;
        }
        if (!board.spotForLegalTilePlacementAvailable()) {
            return true;
        }
        for (PlayerEntry entry : playerTable) {
            if (entry.isPlayerAllowedToPlay()
                    && entry.getTilesInHand()
                            .size() > 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * @return true if a player has reached 18 pts with all colors (game is over
     *         immediately)
     */
    private boolean allColorsOfAPlayerAreMax() {
        for (PlayerEntry playerEntry : playerTable) {
            Entry<IngeniousColor, Integer> lowestScore =
                    playerEntry.getScore()
                            .getIncreasingOrderSortedScore()
                            .get(0);
            if (lowestScore.getValue() >= ScoreCalc.BONUS_SCORE_LINE) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return the number of players in the game that are still allowed to make
     *         a move
     */
    private int getNumberOfQualifiedPlayers() {
        int qualifiedPlayers = 0;
        for (PlayerEntry currentPlayer : playerTable) {
            if (currentPlayer.isPlayerAllowedToPlay()) {
                qualifiedPlayers++;
            }
        }
        return qualifiedPlayers;
    }

    public int getPlayerTableSize() {
        return playerTable.size();
    }

    public int getNbrOfPlayers() {
        return this.nbrOfPlayers;
    }
}
